home
***
CD-ROM
|
disk
|
FTP
|
other
***
search
/
PC World Komputer 2010 April
/
PCWorld0410.iso
/
pluginy Firefox
/
7684
/
7684.xpi
/
resources
/
fmLogin.js
< prev
next >
Wrap
Text File
|
2009-11-20
|
20KB
|
595 lines
/**
* Copyright (c) 2008, Jose Enrique Bolanos, Jorge Villalobos
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* * Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
* * Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
* * Neither the name of Jose Enrique Bolanos, Jorge Villalobos nor the names
* of its contributors may be used to endorse or promote products derived
* from this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER
* OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
**/
var EXPORTED_SYMBOLS = [];
const Cc = Components.classes;
const Ci = Components.interfaces;
Components.utils.import("resource://firefm/fmCommon.js");
Components.utils.import("resource://firefm/fmSecret.js");
// Observer topic for changed cookies. We use this to detect if we're logged in
// with Last.FM or not.
const TOPIC_COOKIE_CHANGED = "cookie-changed";
// Base 64 characters.
const BASE64 =
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=";
// The data used to store and retrieve the Last.fm API session key.
const API_SESSION_LOGIN_HOST = "chrome://firefm";
const API_SESSION_LOGIN_REALM = "Last.fm Web Services";
// Last.fm URLs.
const URL_BASE = "http://www.last.fm";
const URL_BASE_SSL = "https://www.last.fm";
const URL_EXT_BASE = "http://ext.last.fm";
const URL_RPC = URL_EXT_BASE + "/1.0/webclient/xmlrpc.php";
const URL_HANDSHAKE = URL_EXT_BASE + "/1.0/radio/webclient/handshake.php";
// Data payloads for different API calls.
const PARAMS_GET_SESSION =
"<methodCall><methodName>getSession</methodName><params /></methodCall>";
const PARAMS_HANDSHAKE = "?sessionKey=$(SESSION)&user=$(USER)";
const PARAMS_ADJUST = "?lang=en&session=$(SESSION)&url=$(URL)&user=$(USER)";
const PARAMS_PLAYLIST = "?sk=$(SESSION)&fod=true&y=$(TIMESTAMP)";
// Regular expression to extract information from the handshake response.
const RE_RESPONSE_HANDSHAKE =
/^session\=([^\&]+)\&playlist\_url\=([^\&]+)\&subscriber\=([^\&]+)\&base\_url\=([^\&]+)\&base\_path\=([^\&]+)\&$/;
// The amount of time to wait to check the logged in state at startup.
const LOGGED_IN_TIMEOUT = 4 * 1000; // 4 seconds.
// The amount of time to wait to show the API notification.
const API_NOTIFICATION_TIMEOUT = 7 * 1000; // 7 seconds.
// The amount of time to wait before doing the getSession call after checking
// the Last.fm cookie.
const GET_SESSION_TIMEOUT = 250; // 250 ms.
/**
* Handles the authentication against the Last.fm API, and the fetching of
* playlists.
*/
FireFM.Login = {
// Topic notifications sent from this object.
get TOPIC_USER_AUTHENTICATION() { return "firefm-user-authentication"; },
/* Home URL. */
get URL_HOME() { return URL_BASE; },
/* Login URL. */
get URL_LOGIN() {
let url = URL_BASE_SSL + "/login";
// point to the API permission page when doing the first login, or when
// trying to login and there's already a user logged in.
if (!this._useNormalLogin || (null != this._userName)) {
url = this.URL_API_ACCESS;
this._useNormalLoginPref.value = true;
this._useNormalLogin = true;
}
return url;
},
/* Logout URL. */
get URL_LOGOUT() { return URL_BASE + "/login/logout"; },
/* API access approval URL. */
get URL_API_ACCESS() {
if (null == this._apiAccessURL) {
this._apiAccessURL =
(URL_BASE + "/api/auth?api_key=" + FireFM.Secret.API_KEY);
}
return this._apiAccessURL;
},
/* Regular expression that identifies the API access URL for all localizations
of the Last.fm site. */
get URL_API_ACCESS_RE() {
if (null == this._apiAccessURLRE) {
this._apiAccessURLRE =
new RegExp(
("^http(?:s)?://(?:[a-z]+\\.)?last(?:\\.)?fm(?:[a-z\\.]+)?/api/auth" +
"\\?api_key=" + FireFM.Secret.API_KEY),
"i");
}
return this._apiAccessURLRE;
},
/* Login Manager service reference. */
_loginManager : null,
/* Logger for this object. */
_logger : null,
/* The name of the currently logged in user. */
_userName : null,
/* The api session of the currently logged in user. */
_apiSession : null,
/* Preference that indicates if the login URL should point to its normal
location or the web services page where the user can enable Fire.fm use. */
_useNormalLoginPref : null,
/* Current value of the normal login URL preference. */
_useNormalLogin : false,
/* The API access approval URL. */
_apiAccessURL : null,
/* The API access approval URL regular expression. */
_apiAccessURLRE : null,
/**
* Initializes this object.
*/
init : function() {
let timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
let that = this;
this._logger = FireFM.getLogger("FireFM.Login");
this._logger.debug("init");
this._loginManager =
Cc["@mozilla.org/login-manager;1"].getService(Ci.nsILoginManager);
this._useNormalLoginPref =
FireFM.Application.prefs.get(FireFM.PREF_BRANCH + "useNormalLoginURL");
this._useNormalLogin = this._useNormalLoginPref.value;
// set the logged in state.
// XXX: we do this in a timeout to prevent the Master Password prompt from
// appearing before the initial Firefox window.
timer.initWithCallback(
{ notify : function() {
that._checkLoggedInState();
FireFM.obsService.addObserver(that, TOPIC_COOKIE_CHANGED, false);
} },
LOGGED_IN_TIMEOUT, Ci.nsITimer.TYPE_ONE_SHOT);
},
/**
* Gets the user name of the currently logged in user.
* @return the user name of the currently logged in user. null if no user is
* online.
*/
get userName() {
this._logger.debug("[getter] userName");
return this._userName;
},
/**
* Get the API session key for the currently logged in user.
* @return the API session key for the currently logged in user. null if no
* user is logged in or no key is stored for this user.
*/
get apiSession() {
this._logger.debug("[getter] apiSession");
if (null != this._userName) {
if (null == this._apiSession) {
let userLower = this._userName.toLowerCase();
let loginObjs = [];
let loginCount;
try {
loginObjs =
this._loginManager.findLogins(
{}, API_SESSION_LOGIN_HOST, null, API_SESSION_LOGIN_REALM);
} catch (e) {
this._logger.warn(
"[getter] apiSession. User rejected Master Password prompt.\n" + e);
}
loginCount = loginObjs.length;
this._logger.debug("[getter] apiSession. Login count: " + loginCount);
for (let i = 0; i < loginCount; i++) {
if (userLower == loginObjs[i].username.toLowerCase()) {
this._apiSession = loginObjs[i].password;
break;
}
}
}
} else {
this._apiSession = null;
}
return this._apiSession;
},
/**
* Set the API session key for the currently logged in user.
* @param aValue the API session key for the currently logged in user.
*/
set apiSession(aValue) {
this._logger.debug("[setter] apiSession");
let nsLoginInfo =
new Components.Constructor(
"@mozilla.org/login-manager/loginInfo;1", Ci.nsILoginInfo, "init");
let loginObj =
new nsLoginInfo(
API_SESSION_LOGIN_HOST, null, API_SESSION_LOGIN_REALM, this._userName,
aValue, "", "");
try {
this._loginManager.addLogin(loginObj);
} catch (e) {
this._logger.warn(
"[setter] apiSession. User rejected Master Password prompt.\n" + e);
}
this._apiSession = aValue;
FireFM.obsService.notifyObservers(
null, this.TOPIC_USER_AUTHENTICATION, this._userName);
},
/**
* Checks the state of the Last.FM session cookie at startup.
*/
_checkLoggedInState : function() {
this._logger.trace("_checkLoggedInState");
let cookieManager =
Cc["@mozilla.org/cookiemanager;1"].getService(Ci.nsICookieManager);
let cookies = cookieManager.enumerator;
let found = false;
let cookie;
while (cookies.hasMoreElements()) {
cookie = cookies.getNext();
if (this._isLastFMSessionCookie(cookie)) {
found = true;
break;
}
}
if (found) {
this._logger.debug("_checkLoggedInState. Logged in.");
this._beginLogin();
} else {
this._logger.debug("_checkLoggedInState. Logged out.");
FireFM.obsService.notifyObservers(
null, this.TOPIC_USER_AUTHENTICATION, null);
}
},
/**
* Indicates if the given cookie is the Last.FM session cookie, used to know
* if the user is logged in or not.
* @param aCookie the cookie to check.
* @return true if the cookie is the Last.FM session cookie, false otherwise.
*/
_isLastFMSessionCookie : function(aCookie) {
// XXX: there is no logging here for performance purposes.
let isCookie =
((aCookie instanceof Ci.nsICookie) && (".last.fm" == aCookie.host) &&
("Session" == aCookie.name));
return isCookie;
},
/**
* Begins the login process for the extension.
*/
_beginLogin : function() {
this._logger.trace("_beginLogin");
let timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
let that = this;
this._userName = null;
this._apiSession = null;
// XXX: we need a timeout here for 2 reasons: (1) there are some random
// problems occurring in general right after we check the cookie, and (2)
// we experience a similar problem consistently after returning from Private
// Browsing mode.
timer.initWithCallback(
{ notify : function() { that._getSession(); } }, GET_SESSION_TIMEOUT,
Ci.nsITimer.TYPE_ONE_SHOT);
// we don't need to use the special login page for users that were already
// logged in.
if (!this._useNormalLogin) {
this._useNormalLoginPref.value = true;
this._useNormalLogin = true;
}
},
/**
* Requests a session from Last.fm to get the currently logged in user name.
*/
_getSession : function() {
this._logger.trace("_getSession");
let that = this;
let inputStream = this._convertToStream(PARAMS_GET_SESSION);
this._sendRequest(
URL_RPC, function(aEvent) { that._getSessionLoad(aEvent); },
function(aEvent) { that._getSessionError(aEvent); }, null,
true, inputStream);
},
/**
* Load callback handler for the get session request.
* @param aEvent the event that triggered this function.
*/
_getSessionLoad : function(aEvent) {
this._logger.trace("_getSessionLoad");
try {
let doc = aEvent.target.responseXML;
let strings = doc.getElementsByTagName("string");
let user = strings[0].textContent;
this._logger.debug("_getSessionLoad. User: " + user);
if ((0 < user.length) && ("LFM_ANON" != user)) {
let apiSession;
this._userName = user;
apiSession = this.apiSession; // _username needs to be set to do this!
if (null == apiSession) {
let timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
let that = this;
// XXX: we need a timeout in this case because we may be in the
// middle of the login process, which may clear our notification
// due to the tab changing location.
timer.initWithCallback(
{ notify : function() { that._showAPINotification(); } },
API_NOTIFICATION_TIMEOUT, Ci.nsITimer.TYPE_ONE_SHOT);
} else {
FireFM.obsService.notifyObservers(
null, this.TOPIC_USER_AUTHENTICATION, user);
// request a Scrobble session.
FireFM.Remote.scrobbleHandshake();
}
} else {
this._userName = null;
this._apiSession = null;
FireFM.obsService.notifyObservers(
null, this.TOPIC_USER_AUTHENTICATION, null);
}
} catch (e) {
this._logger.error(
"_getSessionLoad. Invalid data received: " +
aEvent.target.responseText + "\nError:\n" + e);
this._getSessionError(aEvent, aIsGetUser);
}
},
/**
* Makes the main window display the API permission notification, if
* necessary. It will avoid doing it if the permission page is already open.
*/
_showAPINotification : function() {
this._logger.trace("_showAPINotification");
let wm =
Cc["@mozilla.org/appshell/window-mediator;1"].
getService(Ci.nsIWindowMediator);
let win = wm.getMostRecentWindow("navigator:browser");
let contentDoc = win.gBrowser.contentDocument;
// only show this notification when not displaying the permission page. This
// can happen the first time the user clicks on the login button after
// installing.
if (!contentDoc || (this.URL_API_ACCESS != contentDoc.documentURI)) {
// tell the user to give permission to Fire.fm.
win.FireFMChrome.BrowserOverlay.showAPINotification();
}
},
/**
* Error callback handler for the get session request.
* @param aEvent the event that triggered this function.
*/
_getSessionError : function(aEvent) {
this._logger.error("_getSessionError");
this._userName = null;
this._apiSession = null;
FireFM.obsService.notifyObservers(
null, this.TOPIC_USER_AUTHENTICATION, null);
this._defaultError("getSession", aEvent);
},
/**
* Sends an HTTP request. This is just an utility function to save some code
* lines.
* @param aURL the url to send the request to.
* @param aLoadHandler the load callback handler. Can be null.
* @param aErrorHandler the error callback handler. Can be null.
* @param aHeaders object mapping that represents the headers to send. Can be
* null or empty.
* @param aIsPOST indicates if the method POST (true) or GET (false).
* @param aPOSTString the string or stream to send through post (optional).
*/
_sendRequest : function(
aURL, aLoadHandler, aErrorHandler, aHeaders, aIsPOST, aPOSTString) {
this._logger.trace("_sendRequest");
let request =
Cc["@mozilla.org/xmlextras/xmlhttprequest;1"].createInstance();
// add event handlers.
request.QueryInterface(Ci.nsIDOMEventTarget);
if (null != aLoadHandler) {
request.addEventListener("load", aLoadHandler, false);
}
if (null != aErrorHandler) {
request.addEventListener("error", aErrorHandler, false);
}
// prepare and send the request.
request.QueryInterface(Ci.nsIXMLHttpRequest);
request.open((aIsPOST ? "POST" : "GET"), aURL, true);
if (null != aHeaders) {
for (let header in aHeaders) {
request.setRequestHeader(header, aHeaders[header]);
}
}
if (aIsPOST) {
request.send(aPOSTString);
} else {
request.send(null);
}
},
/**
* Converts the given string into a UTF-8 string that can be sent through POST
* as if it were binary. This is required for several Last.fm calls.
* @param aString the string to convert into a stream.
* @return nsIInputStream for the given string.
*/
_convertToStream : function(aString) {
this._logger.trace("_convertToStream");
let multiStream =
Cc["@mozilla.org/io/multiplex-input-stream;1"].
createInstance(Ci.nsIMultiplexInputStream);
let converter =
Cc["@mozilla.org/intl/scriptableunicodeconverter"].
createInstance(Ci.nsIScriptableUnicodeConverter);
let inputStream;
converter.charset = "UTF-8";
inputStream = converter.convertToInputStream(aString);
multiStream.appendStream(inputStream);
return multiStream;
},
/**
* Default error callback handler for the asynchronous requests.
* @param aSource a string that identifies the source of the error.
* @param aEvent the event that triggered this function.
*/
_defaultError : function(aSource, aEvent) {
this._logger.debug("_defaultError");
try {
this._logger.error(
"_defaultError. Source: " + aSource + ", status: " +
aEvent.target.status + ", response: " + aEvent.target.responseText);
} catch (e) {
this._logger.error("_defaultError. Error:\n" + e);
}
},
/**
* Decodes a Base64 encoded string and returns the clear text version.
* @param aEncodedString a Base64 encoded string.
* @return clear text contents of the Base64 string.
* @throws Exception if the input string is badly formatted.
*/
_decode : function(aEncodedString) {
this._logger.debug("_decode");
let src = aEncodedString;
let decoded = "";
let pos = 0;
let v1, v2, v3, v4, v5, v6, v7;
while (pos < src.length) {
v5 = BASE64.indexOf(src.charAt(pos++));
v3 = BASE64.indexOf(src.charAt(pos++));
v1 = BASE64.indexOf(src.charAt(pos++));
v2 = BASE64.indexOf(src.charAt(pos++));
v4 = v5 << 2 | v3 >> 4;
v7 = (v3 & 15) << 4 | v1 >> 2;
v6 = (v1 & 3) << 6 | v2;
decoded += String.fromCharCode(v4);
if (v1 != 64) {
decoded += String.fromCharCode(v7);
}
if (v2 != 64) {
decoded += String.fromCharCode(v6);
}
}
return FireFM.decodeFMString(decoded);
},
/**
* Observes notifications of cookie and track activity.
* @param aSubject The object that experienced the change.
* @param aTopic The topic being observed.
* @param aData The data related to the change.
*/
observe : function(aSubject, aTopic, aData) {
// XXX: there is no logging here for performance purposes.
if (TOPIC_COOKIE_CHANGED == aTopic) {
switch (aData) {
case "added":
if (this._isLastFMSessionCookie(aSubject)) {
this._logger.debug("observe. Logged in.");
this._beginLogin();
}
break;
case "deleted":
if (this._isLastFMSessionCookie(aSubject)) {
this._logger.debug("observe. Cookie deleted.");
this._userName = null;
this._apiSession = null;
// We used to just log a user out in this case, but there seems to
// be cases where the a 'Session' cookie is removed right after a
// new one has been set.
this._checkLoggedInState();
}
break;
case "cleared":
case "reload":
this._logger.info("observe. Cookie list reset.");
this._userName = null;
this._apiSession = null;
this._checkLoggedInState();
break;
}
}
}
};
/**
* FireFM.Login constructor.
*/
(function() {
this.init();
}).apply(FireFM.Login);